package RE.impl.ex3;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import RE.api.*;
import RE.rep.*;
import RE.rep.TokenWithNER.NamedEntityType;

/**
 * @author zirolny 30795560 charika 30534360
 */
public class Ex3 implements RelationExtractor<SentenceWithDependenciesAndNER>, RelationEvaluator {

	// ========= Implement the methods required by the RelationEvaluator interface ======

	@Override
	public List<Relation> loadAnnotation(String filename) {
		// Create the list of the relations
		List<Relation> list = new ArrayList<Relation>();
		try {
			// Get a reader from the file
			BufferedReader reader = new BufferedReader(new FileReader(filename));
			String line;
			// Read all the lines
			while ((line = reader.readLine()) != null) {
				// Split the line by tab
				String[] elements = line.split("\t");
				/*
				// Get only some of the relations
				if (elements[2].intern() != "Kill")
					continue;
				*/
				// Create the relation based on the line read
				Relation rel = new Relation(Relation.RelationName.valueOf(elements[2]), elements[1], elements[3], elements[0]);
				if(!isCorrectlyExtractedRelation(rel,list))
					list.add(rel);
			}
			// Close the reader
			reader.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		// Return the list of relations
		return list;
	}

	@Override
	public boolean isMatch(Relation relA, Relation relB) {
		// Return true if all the relation attributes are the same		
		return ((relA.getRelName() == relB.getRelName()) &&
				(relA.getSentenceId().intern() == relB.getSentenceId().intern()) &&
				(relA.getArg1().intern() == relB.getArg1().intern()) &&
				(relA.getArg2().intern() == relB.getArg2().intern()));
	}

	@Override
	public boolean isCorrectlyExtractedRelation(Relation relation, List<Relation> annotation) {
		// Check all the relations in the list
		for (Relation rel : annotation) {
			
			
			// Check if the current relation matches the given relation
			if (isMatch(rel, relation))
				return true;
		}
		// Couldn't find a relation that matches the given relation
		return false;
	}

	@Override
	public Double getRecall(Integer numberOfCorrectlyExtractedRelations, Integer numberOfGoldStandardRelationsToBeExtracted) {
		// Return the recall
		return numberOfCorrectlyExtractedRelations.doubleValue() / numberOfGoldStandardRelationsToBeExtracted;
	}

	@Override
	public Double getPrecision(Integer numberOfCorrectlyExtractedRelations, Integer numberOfAllExtractedRelations) {
		// Return the precision
		return numberOfCorrectlyExtractedRelations.doubleValue() / numberOfAllExtractedRelations;
	}

	@Override
	public Double getF1(Double recall, Double precision) {
		// Return the F1
		return (2 * precision * recall) / (precision + recall);
	}

	// ========= Implement the method required by the RelationExtractor interface ======

	@Override
	public List<Relation> extractRelations(SentenceWithDependenciesAndNER sentence, Configuration<SentenceWithDependenciesAndNER> conf) {
		// Create the list of the relations
		List<Relation> relations = new ArrayList<Relation>();
		// Get the configuration rules
		List<Rule<SentenceWithDependenciesAndNER>> rules = conf.getConfiguration();
		// Apply all the rules on the sentence
		for (Rule<SentenceWithDependenciesAndNER> rule : rules) {
			List<Relation> curLst = rule.applyRule(sentence);
			for (Relation rel : curLst) {
				boolean exists = false;
				for (Relation rel2 : relations) {
					if (isMatch(rel, rel2)) {
						exists = true;
						break;
					}
				}
				if (!exists)
					relations.add(rel);
			}
		}
		return relations;
	}

	// ========= Implement the main method ========

	/**
	 * @param args - arg[0] - "Corpus" file name; arg[1] - "Annotation" file name; arg[2] - configuration type; arg[3] - output file name
	 */
	public static void main(String[] args) {
		// Create the configuration
		Configuration<SentenceWithDependenciesAndNER> conf;
		if (args[2].intern() == "best")
			conf = new Ex3BestConfiguration();
		else
			conf = new Ex3FullConfiguration();
		conf.generateConfiguration();
		// Get the relations from the corpus file
		List<Relation> relations = extractCorpusRelations(args[0], conf);
		// Create an instance of Ex3
		Ex3 e = new Ex3();
		// Get the relations from the annotation file
		List<Relation> annotation = e.loadAnnotation(args[1]);
		try {
			// Get a writer to the output file
			PrintWriter writer = new PrintWriter(args[3]);
			int correctRelationsNo = 0; 
			// Print all the relations 
			for (Relation relation : relations) { 
				// Check if this relation is also found in the annotation 
				if(e.isCorrectlyExtractedRelation(relation, annotation))
					correctRelationsNo++;
				// Print the relation 
				writer.println(relation); 
			}
			// Calculate the recall 
			double recall = e.getRecall(correctRelationsNo, annotation.size()); 
			// Calculate the precision 
			double precision = e.getPrecision(correctRelationsNo, relations.size());
			// Calculate the F1
			double f1 = e.getF1(recall, precision); 
			// Print the recall, precision and F1
			writer.println(recall + "\t" + precision + "\t" + f1);
			// Close the writer
			writer.close();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	/**
	 * Gets the all the relations from the given corpus file.
	 * 
	 * @param filename
	 *            The corpus filename.
	 * @param conf
	 *            The configuration to use.
	 * @return A list of relations extracted from the file.
	 */
	private static List<Relation> extractCorpusRelations(String filename, Configuration<SentenceWithDependenciesAndNER> conf) {
		String text;
		try {
			// Get a reader to the corpus file
			BufferedReader reader = new BufferedReader(new FileReader(filename));
			// Read all the lines of the file
			String line;
			StringBuilder textBuffer = new StringBuilder();
			while ((line = reader.readLine()) != null)
				textBuffer.append(line);
			// Close the reader
			reader.close();
			text = textBuffer.toString();
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
		// Remove the spaces between the tags
		text = trimSpaces(text);
		// Get the sentences from the text
		List<SentenceWithDependenciesAndNER> sentences = extractSentences(text);
		// Create an instance of Ex3
		Ex3 e = new Ex3();
		// Get the relations of each sentence
		List<Relation> relations = new ArrayList<Relation>();
		for (SentenceWithDependenciesAndNER sent : sentences)
			relations.addAll(e.extractRelations(sent, conf));
		// Return the relations
		return relations;
	}

	/**
	 * Removes the spaces between the tags.
	 * 
	 * @param text
	 *            The text to remove from.
	 * @return The text without the spaces.
	 */
	private static String trimSpaces(String text) {
		// Create the string buffer
		StringBuilder retStr = new StringBuilder();
		// Check all the chars in the text
		for (int i = 0; i < text.length(); i++) {
			// Get the current char
			char ch = text.charAt(i);
			// Check if the char is space or tab
			if ((ch == ' ') || (ch == '\t')) {
				boolean startingTag = false;
				// Check all the previous chars
				for (int j = i - 1; j >= 0; j--) {
					// Get the current char
					char ch2 = text.charAt(j);
					// Check if it is a starting tag
					if (ch2 == '<') {
						startingTag = true;
						break;
						// Check if it is an ending char
					} else if (ch2 == '>') {
						startingTag = false;
						break;
					}
				}
				// Check if the char is inside a tag
				if (!startingTag)
					continue;
			}
			// Add the char to the new string
			retStr.append(ch);
		}
		// Return the new string
		return retStr.toString();
	}

	/**
	 * Gets the sentences of the given text.
	 * 
	 * @param text
	 *            The text to get the sentences from.
	 * @return A list of sentences in the text.
	 */
	private static List<SentenceWithDependenciesAndNER> extractSentences(String text) {
		// Create the list of sentences
		List<SentenceWithDependenciesAndNER> sentencesList = new ArrayList<SentenceWithDependenciesAndNER>();
		// Create the pattern of the whole file
		String filePat = "<\\?xml version=\"1.0\" encoding=\"UTF-8\"\\?>"
				+ "<\\?xml-stylesheet href=\"CoreNLP-to-HTML.xsl\" type=\"text/xsl\"\\?>"
				+ "<root>" + "<document>" + "<sentences>" + "(.*)"
				+ "</sentences>" + "</document>" + "</root>";
		// Compile and evaluate the pattern
		Pattern filePattern = Pattern.compile(filePat);
		Matcher fileMatcher = filePattern.matcher(text);
		fileMatcher.matches();
		// Get the sentences part
		String sentences = fileMatcher.group(1);

		// Create the pattern of a single sentence
		String sentencePat = "<sentence id=\"([0-9]*)\">" + "<tokens>" + "(("
				+ "<token id=\"[0-9]*\">" + "<word>[^<]*</word>"
				+ "<lemma>[^<]*</lemma>"
				+ "<CharacterOffsetBegin>[0-9]*</CharacterOffsetBegin>"
				+ "<CharacterOffsetEnd>[0-9]*</CharacterOffsetEnd>"
				+ "<POS>[^<]*</POS>" + "<NER>[^<]*</NER>"
				+ "(<NormalizedNER>[^<]*</NormalizedNER>)?"
				+ "((<Timex tid=\"[^<]*\" type=\"[^<]*\"/>)|"
				+ "(<Timex tid=\"[^<]*\" type=\"[^<]*\">[^<]*</Timex>))?"
				+ "</token>" + ")*)" + "</tokens>" + "<parse>[^<]*</parse>"
				+ "<basic-dependencies>" + "((" + "<dep type=\"[^<]*\">"
				+ "<governor idx=\"[0-9]*\">[^<]*</governor>"
				+ "<dependent idx=\"[0-9]*\">[^<]*</dependent>" + "</dep>"
				+ ")*)" + "</basic-dependencies>" + "<collapsed-dependencies>"
				+ "((" + "<dep type=\"[^<]*\">"
				+ "<governor idx=\"[0-9]*\">[^<]*</governor>"
				+ "<dependent idx=\"[0-9]*\">[^<]*</dependent>" + "</dep>"
				+ ")*)" + "</collapsed-dependencies>"
				+ "<collapsed-ccprocessed-dependencies>" + "(("
				+ "<dep type=\"[^<]*\">"
				+ "<governor idx=\"[0-9]*\">[^<]*</governor>"
				+ "<dependent idx=\"[0-9]*\">[^<]*</dependent>" + "</dep>"
				+ ")*)" + "</collapsed-ccprocessed-dependencies>"
				+ "</sentence>";
		// Compile the pattern
		Pattern sentencePattern = Pattern.compile(sentencePat);
		Matcher sentenceMatcher = sentencePattern.matcher(sentences);

		// Continue while there are more sentences
		while (sentenceMatcher.find()) {
			// Create the pattern of a single token
			String tokenPat = "<token id=\"([0-9]*)\">"
					+ "<word>([^<]*)</word>" + "<lemma>([^<]*)</lemma>"
					+ "<CharacterOffsetBegin>[0-9]*</CharacterOffsetBegin>"
					+ "<CharacterOffsetEnd>[0-9]*</CharacterOffsetEnd>"
					+ "<POS>([^<]*)</POS>" + "<NER>([^<]*)</NER>"
					+ "(<NormalizedNER>[^<]*</NormalizedNER>)?"
					+ "((<Timex tid=\"[^<]*\" type=\"[^<]*\"/>)?|"
					+ "(<Timex tid=\"[^<]*\" type=\"[^<]*\">[^<]*</Timex>)?)"
					+ "</token>";
			// Compile the pattern
			Pattern tokenPattern = Pattern.compile(tokenPat);
			Matcher tokenMatcher = tokenPattern.matcher(sentenceMatcher.group(2));
			String sentenceID = "";
			StringBuilder xmlFormattedSentence = new StringBuilder();
			ArrayList<TokenWithNER> tokens = new ArrayList<TokenWithNER>();
			// Get all the tokens
			while (tokenMatcher.find()) {
				// Get the token word
				String word = tokenMatcher.group(2);
				if (word.matches("sent[0-9]+") && sentenceID.intern() == "")
					sentenceID = word;
				// Check if inside a sentence
				if (sentenceID.length() == 0)
					continue;
				// Get the token ID
				int tokenID = Integer.valueOf(tokenMatcher.group(1));
				// Get the token lemma
				String lemma = tokenMatcher.group(3);
				// Get the token POS
				String posStr = tokenMatcher.group(4);
				POS pos;
				try {
					pos = POS.valueOf(posStr);
				} catch (Exception e) {
					pos = POS.OTHER;
				}
				String NERStr = tokenMatcher.group(5);
				NamedEntityType ner;
				try {
					ner = NamedEntityType.valueOf(NERStr);
				} catch (Exception e) {
					ner = NamedEntityType.NOT_DEFINED;
				}
				// Create the token
				TokenWithNER token = new TokenWithNER(tokenID, sentenceID, word, lemma, pos, ner);
				// Add the token to the tokens list
				tokens.add(token);
				// Add the token to the XML string
				xmlFormattedSentence.append(tokenMatcher.group(0));
			}

			// String parsePat="<parse>([^<])*</parse>";

			String depPat = "<dep type=\"([^<]*)\">"
					+ "<governor idx=\"([0-9]*)\">[^<]*</governor>"
					+ "<dependent idx=\"([0-9]*)\">[^<]*</dependent>"
					+ "</dep>";

			// Compile the pattern
			Pattern depPattern = Pattern.compile(depPat);
			Matcher basicDepMatcher = depPattern.matcher(sentenceMatcher.group(8));

			List<SyntacticDependency> dependencies = new ArrayList<SyntacticDependency>();
			// Get all the basic dependencies
			while (basicDepMatcher.find()) {
				String depType = basicDepMatcher.group(1);
				Integer governorIdx = Integer.parseInt(basicDepMatcher.group(2));
				Integer dependentIdx = Integer.parseInt(basicDepMatcher.group(3));
				TokenWithNER governorTok = null, dependentTok = null;
				for (TokenWithNER tok : tokens) {
					if (tok.getTokenId() == governorIdx)
						governorTok = tok;
					if (tok.getTokenId() == dependentIdx)
						dependentTok = tok;
				}
				DependencyName dep;
				try {
					dep = DependencyName.valueOf(depType);
				} catch (Exception e) {
					dep = null;
				}
				if ((dep != null) && (governorTok != null) && (dependentTok != null))
					dependencies.add(new SyntacticDependency(dep, governorTok, dependentTok));

			}

			Matcher collapsedDepMatcher = depPattern.matcher(sentenceMatcher.group(10));
			// Get all the collapsed dependencies
			while (collapsedDepMatcher.find()) {
				String depType = collapsedDepMatcher.group(1);
				Integer governorIdx = Integer.parseInt(collapsedDepMatcher.group(2));
				Integer dependentIdx = Integer.parseInt(collapsedDepMatcher.group(3));
				TokenWithNER governorTok = null, dependentTok = null;
				for (TokenWithNER tok : tokens) {
					if (tok.getTokenId() == governorIdx)
						governorTok = tok;
					if (tok.getTokenId() == dependentIdx)
						dependentTok = tok;
				}
				DependencyName dep;
				try {
					dep = DependencyName.valueOf(depType);
				} catch (Exception e) {
					dep = null;
				}
				if ((dep != null) && (governorTok != null) && (dependentTok != null))
					dependencies.add(new SyntacticDependency(dep, governorTok,dependentTok));
			}

			Matcher ccprocessedDepMatcher = depPattern.matcher(sentenceMatcher.group(12));
			// Get all the ccprocessed dependencies
			while (ccprocessedDepMatcher.find()) {
				String depType = ccprocessedDepMatcher.group(1);
				Integer governorIdx = Integer.parseInt(ccprocessedDepMatcher.group(2));
				Integer dependentIdx = Integer.parseInt(ccprocessedDepMatcher.group(3));
				TokenWithNER governorTok = null, dependentTok = null;
				for (TokenWithNER tok : tokens) {
					if (tok.getTokenId() == governorIdx)
						governorTok = tok;
					if (tok.getTokenId() == dependentIdx)
						dependentTok = tok;
				}
				DependencyName dep;
				try {
					dep = DependencyName.valueOf(depType);
				} catch (Exception e) {
					dep = null;
				}
				if ((dep != null) && (governorTok != null) && (dependentTok != null))
					dependencies.add(new SyntacticDependency(dep, governorTok, dependentTok));
			}
			// Add the last sentence if exists
			if (sentenceID.length() > 0)
				sentencesList.addAll(splitSentence(xmlFormattedSentence.toString(), tokens, dependencies));
		}

		// Return the sentences list
		return sentencesList;

	}

	private static List<SentenceWithDependenciesAndNER> splitSentence(String xmlFormattedSentence, List<TokenWithNER> tokens, List<SyntacticDependency> dependencies) {
		ArrayList<SentenceWithDependenciesAndNER> sentences = new ArrayList<SentenceWithDependenciesAndNER>();
		int i = 0;
		while ((i < tokens.size()) && (!tokens.get(i).getWord().matches("sent[0-9]+")))
			i++;
		if (i == tokens.size())
			return new ArrayList<SentenceWithDependenciesAndNER>();
		while (true) {
			int j = i + 1;
			while ((j < tokens.size()) && (!tokens.get(j).getWord().matches("sent[0-9]+")))
				j++;
			List<TokenWithNER> sentenceTokens = tokens.subList(i, j);
			for (TokenWithNER token : sentenceTokens)
				token.setSentenceId(tokens.get(i).getWord());
			ArrayList<SyntacticDependency> sentenceDependencies = new ArrayList<SyntacticDependency>();
			for (SyntacticDependency dependency : dependencies) {
				if ((dependency.getGovernorToken().getTokenId() > i)
						&& (dependency.getGovernorToken().getTokenId() <= j)
						&& (dependency.getDependentToken().getTokenId() > i)
						&& (dependency.getDependentToken().getTokenId() <= j)) {
					sentenceDependencies.add(dependency);
				}
			}
			sentences.add(new SentenceWithDependenciesAndNER(xmlFormattedSentence, sentenceTokens, sentenceDependencies));
			if (j == tokens.size())
				break;
			i = j;
		}
		return sentences;
	}
}
